package com.seednia.foundation.framework.dto;

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.beans.PropertyVetoException;
import java.beans.VetoableChangeListener;
import java.beans.VetoableChangeSupport;
import java.io.IOException;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import javax.persistence.Id;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.type.TypeReference;

import com.seednia.foundation.framework.dto.annotation.IndexableField;


/**
 * This is the base class of all DTO (Data Transfer Object), aka entity objects  or records
 * used in the system.
 * 
 * @author Young Suk Ahn
 * 
 */
@XmlRootElement(name = "AbstractRecord", 
		namespace = "http://seednia.com/foundation/1.0")
// maps this class to a generated schema type
@XmlType(name = "AbstractRecord")
// bind all non-static, non-transient fields
// to XML unless annotated with @XmlTransient
@XmlAccessorType(XmlAccessType.PROPERTY)
public abstract class AbstractRecord implements Serializable, Cloneable {

	/**
	 * serial version unique id
	 */
	private static final long serialVersionUID = 3851471789777653790L;

	/**
	 * The ID of the domain that this record is context of
	 **/
	protected String domainId = null;

	/**
	 * Unique id of the object. If two objects of same type have the same sid, 
	 * we can assume that the objects are equal
	 **/
	protected Long sid = Constants.SID_NIL;

	/**
	 * Universal unique id of the object. If two objects of same uuid, 
	 * we can assume that the objects are equal.
	 * This field is not required on all entity object types.
	 **/
	protected String uuid = null;

	/**
	 * Date that the record was created
	 */
	protected Date createDt = new Date();
	
	/**
	 * Number of time the record was modified
	 */
	protected int modifyCounter = 0; /* Number of times modified  */ 

	/**
	 * Last timestamp when the record was modified
	 */
	protected Date lastModifiedDt = new Date(); /* Time stamp of the last modification of the record  */ 
	
	/**
	 * Params used to provide further modifier to the reference
	 * the text is in UUenconded Query string format
	 */
	private String paramsText;
	
	/**
	 * Params in parsed format. Can be used as properties.
	 */
	private Map<String, Object> params = null;
	
	/**
	 * Used to inform that this record is to be deleted.
	 */
	private boolean _markDelete = false;
	
	protected final PropertyChangeSupport pcs = new PropertyChangeSupport( this );

	protected final VetoableChangeSupport vcs = new VetoableChangeSupport( this );
	
	private static final Log logger = LogFactory.getLog(AbstractRecord.class);
	
	// provide default constructor even though this is abstract method.
	// The idea is this class should be a super class of all DTO and not be able
	// to be instantiated.
	public AbstractRecord(){
	}
	
	@XmlAttribute
	@IndexableField(store=IndexableField.Store.YES)
	public String getDomainId() {
		return domainId;
	}
	public void setDomainId(String domainId) {
		this.domainId = domainId;
	}


	/**
	 * this variable is true when a record has been selected in user interface
	 * 
	 */

	@XmlAttribute
	@Id
	@IndexableField(store=IndexableField.Store.YES)
	public Long getSid() {
		return sid;
	}
	public void setSid(Long sid) {
		this.sid = sid;
	}
	

	public String getUuid() {
		return uuid;
	}
	public void setUuid(String uuid) {
		//String old = this.uuid;
        //this.vcs.fireVetoableChange( "uuid", old, uuid );
		this.uuid = uuid;
        //this.pcs.firePropertyChange( "uuid", old, uuid );
	}

	/**
	 * @return the createDate
	 */
	@XmlAttribute
	@IndexableField(store=IndexableField.Store.YES)
	public abstract Long getCreatorSid();
	
	public abstract String getCreatorId();

	/**
	 * @return the createDate
	 */
	@XmlAttribute
	@IndexableField(store=IndexableField.Store.YES)
	public Date getCreateDt() {
		return createDt;
	}

	/**
	 * @param createDate the createDate to set
	 */
	public void setCreateDt(Date createDate) {
		if(createDate == null)
			this.createDt = null;
		else
			this.createDt = new Date(createDate.getTime());
	}

	
	@XmlAttribute
	public int getModifyCounter() {
		return modifyCounter;
	}
	public void setModifyCounter(int modifyCounter) {
		this.modifyCounter = modifyCounter;
	}

	/**
	 * @return the lastModifiedDt
	 */
	@XmlAttribute
	public Date getLastModifiedDt() {
		return lastModifiedDt;
	}

	/**
	 * @param lastModifiedDate the lastModifiedDate to set
	 */
	public void setLastModifiedDt(Date lastModifiedDt) {
		if(lastModifiedDt == null)
			this.lastModifiedDt = null;
		else
			this.lastModifiedDt = new Date(lastModifiedDt.getTime());
	}
	

	/**
	 * Do not use this directly but through getAttribute()
	 * @return
	 */
	public String getParamsText() {
		return paramsText;
	}

	/**
	 * Do not use this directly but through setAttribute()
	 * @return
	 */
	public void setParamsText(String paramsText) {
		this.paramsText = paramsText;
	}

	@XmlTransient
	public Map<String, Object> getParams() {
		if (params == null && this.paramsText != null) {
			ObjectMapper objMapper = new ObjectMapper();
			//params = StringUtils.getQueryMap(this.paramsText);
			try {
				params = objMapper.readValue(this.paramsText, new TypeReference<Map<String, Object>>() {});
			} catch (Exception e) {
				throw new IllegalArgumentException(e);
			}
		}
		return params;
	}

	public void setParams(Map<String, Object> params) {
		this.params = params;
		
		// Reflect the changes to the paramText
		this.serializeParams();
	}
	
	private void serializeParams()
	{
		ObjectMapper objMapper = new ObjectMapper();
		try {
			this.paramsText = objMapper.writeValueAsString(this.params);
		} catch (Exception e) {
			throw new IllegalArgumentException(e);
		}
	}
	
	public Object getParam(String key) {
		if (getParams() != null)
			return getParams().get(key);
		return null;
	}

	public void setParam(String key, Object value)
	{
		if (params == null)
			params = new HashMap<String, Object>();
		params.put(key, value);
		
		// Reflect the changes to the paramText
		serializeParams();
	}
	

	public boolean is_markDelete() {
		return _markDelete;
	}

	public void set_markDelete(boolean _markDelete) {
		this._markDelete = _markDelete;
	}

	/**
	 * This method is used for copy all data from another object.
	 */
	public void copyFrom(AbstractRecord record) {
		this.sid = record.sid;
		this.setCreateDt(record.createDt);
		this.setLastModifiedDt(record.lastModifiedDt);
	}

	/**
	 * This method will compare only with sid and project sid
	 */
	@Override
	public boolean equals(Object obj){
		// if object is null, there is no way they are equal
		if(obj == null)
			return false;
		// They are the same object, pointing to the same reference.
		if(this == obj)
			return true;
		// They are not objects from the same class
		if(!this.getClass().equals(obj.getClass())){
			return false;
		}
		
		// As we previously check class equality, we can make sure that this
		// class cast will not cause any exception 
		AbstractRecord record = (AbstractRecord) obj;
		// for the abstract record, we will check only sid and project sid.
		if( sid == record.getSid() ){
			return true;
		}
		return false;
	}
	
	/*
	@Override
	public int hashCode(){
		int hashCode = 0;
		hashCode = (int)((sid == SID_NIL) ? SID_NIL:sid);
		hashCode = (hashCode<0)?hashCode*-1:hashCode;
		return hashCode;
	}*/
	
	/**
	 * This method is used to create new object which has the exactly data as
	 * this object
	 */
	@Override
	public Object clone() {
		try {
			AbstractRecord retval = (AbstractRecord)BeanUtils.cloneBean(this);
			if (this.createDt != null)
				retval.setCreateDt( (Date)this.createDt.clone());
			if (this.lastModifiedDt != null)
				retval.setLastModifiedDt( (Date)lastModifiedDt.clone());
			
			//if (this.params != null)
			//	retval.setParams( (Map<String, String>)params.clone());
			
			return retval;
		} catch (Exception e) {
			logger.error("Exception at AbstractRecord.clone() ",e); 
			throw new RuntimeException(e);
		}
	}

	
    /**
	 * Registration of the PropertyChangeListener
	 */
	public void addPropertyChangeListener( PropertyChangeListener listener )
    {
        this.pcs.addPropertyChangeListener( listener );
    }
	public void addPropertyChangeListener( String propName, PropertyChangeListener listener )
    {
        this.pcs.addPropertyChangeListener( propName, listener );
    }

    public void removePropertyChangeListener( PropertyChangeListener listener )
    {
        this.pcs.removePropertyChangeListener( listener );
    }
    
    protected void firePropertyChange(String name, Object oldValue, Object newValue) {
    	this.pcs.firePropertyChange(name, oldValue, newValue);
    }

    /**
	 * Registration of the VetoableChangeListener
	 */
    public void addVetoableChangeListener( VetoableChangeListener listener )
    {
        this.vcs.addVetoableChangeListener( listener );
    }

    public void removeVetoableChangeListener( VetoableChangeListener listener )
    {
        this.vcs.removeVetoableChangeListener( listener );
    }
    
    protected void fireVetoableChange(String name, Object oldValue, Object newValue) throws PropertyVetoException {
    	this.vcs.fireVetoableChange(name, oldValue, newValue);
    }
    
    public static AbstractRecord fromString(Class clazz, String jsonStr)
    {
    	ObjectMapper mapper = new ObjectMapper();
    	AbstractRecord retval = null;
        try {
        	retval = mapper.readValue(jsonStr, clazz);
        } catch (IOException e) {
            throw new IllegalArgumentException(e);
        }
        return retval;
    }
    
    @Override
    public String toString()
    {
    	ObjectMapper mapper = new ObjectMapper();
    	String retval = null;
        try {
        	retval = mapper.writeValueAsString(this);
        } catch (IOException e) {
            throw new IllegalArgumentException(e);
        }
        return retval;
    }
}
